/*
 *  Copyright (C) 2009-2010 Howard Chu
 *
 *  This file is part of librtmp.
 *
 *  librtmp is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation; either version 2.1,
 *  or (at your option) any later version.
 *
 *  librtmp is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with librtmp see the file COPYING.  If not, write to
 *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *  Boston, MA  02110-1301, USA.
 *  http://www.gnu.org/copyleft/lgpl.html
 */

#include "../stdafx.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#include "rtmp_sys.h"
#include "log.h"
#include "http.h"

#ifdef CRYPTO
#ifdef USE_POLARSSL
#include <polarssl/sha2.h>
#ifndef SHA256_DIGEST_LENGTH
#define SHA256_DIGEST_LENGTH	32
#endif
#define HMAC_CTX	sha2_context
#define HMAC_setup(ctx, key, len)	sha2_hmac_starts(&ctx, (unsigned char *)key, len, 0)
#define HMAC_crunch(ctx, buf, len)	sha2_hmac_update(&ctx, buf, len)
#define HMAC_finish(ctx, dig, dlen)	dlen = SHA256_DIGEST_LENGTH; sha2_hmac_finish(&ctx, dig)
#define HMAC_close(ctx)
#elif defined(USE_GNUTLS)
#include <gnutls/gnutls.h>
#include <gcrypt.h>
#ifndef SHA256_DIGEST_LENGTH
#define SHA256_DIGEST_LENGTH	32
#endif
#define HMAC_CTX	gcry_md_hd_t
#define HMAC_setup(ctx, key, len)	gcry_md_open(&ctx, GCRY_MD_SHA256, GCRY_MD_FLAG_HMAC); gcry_md_setkey(ctx, key, len)
#define HMAC_crunch(ctx, buf, len)	gcry_md_write(ctx, buf, len)
#define HMAC_finish(ctx, dig, dlen)	dlen = SHA256_DIGEST_LENGTH; memcpy(dig, gcry_md_read(ctx, 0), dlen)
#define HMAC_close(ctx)	gcry_md_close(ctx)
#else	/* USE_OPENSSL */
#include <openssl/ssl.h>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/rc4.h>
#define HMAC_setup(ctx, key, len)	HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, (unsigned char *)key, len, EVP_sha256(), 0)
#define HMAC_crunch(ctx, buf, len)	HMAC_Update(&ctx, (unsigned char *)buf, len)
#define HMAC_finish(ctx, dig, dlen)	HMAC_Final(&ctx, (unsigned char *)dig, &dlen);
#define HMAC_close(ctx)	HMAC_CTX_cleanup(&ctx)
#endif

extern void RTMP_TLS_Init();
extern TLS_CTX RTMP_TLS_ctx;

#include <zlib.h>

#endif /* CRYPTO */

#define	AGENT	"Mozilla/5.0"

HTTPResult
HTTP_get(struct HTTP_ctx *http,const char *url,HTTP_read_callback *cb)
{
	char *host,*path;
	char *p1,*p2;
	char hbuf[256];
	int port=80;
#ifdef CRYPTO
	int ssl=0;
#endif
	int hlen,flen=0;
	int rc,i;
	int len_known;
	HTTPResult ret=HTTPRES_OK;
	struct sockaddr_in sa;
	RTMPSockBuf sb={0};

	http->status= -1;

	memset(&sa,0,sizeof(struct sockaddr_in));
	sa.sin_family=AF_INET;

	/* we only handle http here */
	if(strncasecmp(url,"http",4))
		return HTTPRES_BAD_REQUEST;

	if(url[4]=='s')
	{
#ifdef CRYPTO
		ssl=1;
		port=443;
		if(!RTMP_TLS_ctx)
			RTMP_TLS_Init();
#else
		return HTTPRES_BAD_REQUEST;
#endif
	}

	p1=strchr(url+4,':');
	if(!p1||strncmp(p1,"://",3))
		return HTTPRES_BAD_REQUEST;

	host=p1+3;
	path=strchr(host,'/');
	hlen=path-host;
	strncpy(hbuf,host,hlen);
	hbuf[hlen]='\0';
	host=hbuf;
	p1=strrchr(host,':');
	if(p1)
	{
		*p1++='\0';
		port=atoi(p1);
	}

	sa.sin_addr.s_addr=inet_addr(host);
	if(sa.sin_addr.s_addr==INADDR_NONE)
	{
		struct hostent *hp=gethostbyname(host);
		if(!hp||!hp->h_addr)
			return HTTPRES_LOST_CONNECTION;
		sa.sin_addr= *(struct in_addr*)hp->h_addr;
	}
	sa.sin_port=htons(port);
	sb.sb_socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(sb.sb_socket==-1)
		return HTTPRES_LOST_CONNECTION;
	i=sprintf(sb.sb_buf,"GET %s HTTP/1.0\r\nUser-Agent: %s\r\nHost: %s\r\nReferer: %.*s\r\n",path,AGENT,host,(int)(path-url+1),url);
	if(http->date[0])
		i+=sprintf(sb.sb_buf+i,"If-Modified-Since: %s\r\n",http->date);
	i+=sprintf(sb.sb_buf+i,"\r\n");

	if(connect(sb.sb_socket,(struct sockaddr*)&sa,sizeof(struct sockaddr))<0)
	{
		ret=HTTPRES_LOST_CONNECTION;
		goto leave;
	}
#ifdef CRYPTO
	if(ssl)
	{
#ifdef NO_SSL
		RTMP_Log(RTMP_LOGERROR,"%s, No SSL/TLS support",__FUNCTION__);
		ret=HTTPRES_BAD_REQUEST;
		goto leave;
#else
		TLS_client(RTMP_TLS_ctx,sb.sb_ssl);
		TLS_setfd(sb.sb_ssl,sb.sb_socket);
		if(TLS_connect(sb.sb_ssl)<0)
		{
			RTMP_Log(RTMP_LOGERROR,"%s, TLS_Connect failed",__FUNCTION__);
			ret=HTTPRES_LOST_CONNECTION;
			goto leave;
		}
#endif
	}
#endif
	RTMPSockBuf_Send(&sb,sb.sb_buf,i);

	/* set timeout */
#define HTTP_TIMEOUT	5
	{
		SET_RCVTIMEO(tv,HTTP_TIMEOUT);
		if(setsockopt(sb.sb_socket,SOL_SOCKET,SO_RCVTIMEO,(char*)&tv,sizeof(tv)))
			RTMP_Log(RTMP_LOGERROR,"%s, Setting socket timeout to %ds failed!",__FUNCTION__,HTTP_TIMEOUT);
	}

	sb.sb_size=0;
	sb.sb_timedout=FALSE;
	if(RTMPSockBuf_Fill(&sb)<1)
	{
		ret=HTTPRES_LOST_CONNECTION;
		goto leave;
	}
	if(strncmp(sb.sb_buf,"HTTP/1",6))
	{
		ret=HTTPRES_BAD_REQUEST;
		goto leave;
	}

	p1=strchr(sb.sb_buf,' ');
	rc=atoi(p1+1);
	http->status=rc;

	if(rc>=300)
	{
		if(rc==304)
		{
			ret=HTTPRES_OK_NOT_MODIFIED;
			goto leave;
		}
		else if(rc==404)
			ret=HTTPRES_NOT_FOUND;
		else if(rc>=500)
			ret=HTTPRES_SERVER_ERROR;
		else if(rc>=400)
			ret=HTTPRES_BAD_REQUEST;
		else
			ret=HTTPRES_REDIRECTED;
	}

	p1=memchr(sb.sb_buf,'\n',sb.sb_size);
	if(!p1)
	{
		ret=HTTPRES_BAD_REQUEST;
		goto leave;
	}
	sb.sb_start=p1+1;
	sb.sb_size-=sb.sb_start-sb.sb_buf;

	while((p2=memchr(sb.sb_start,'\r',sb.sb_size)))
	{
		if(*sb.sb_start=='\r')
		{
			sb.sb_start+=2;
			sb.sb_size-=2;
			break;
		}
		else if(!strncasecmp(sb.sb_start,"Content-Length: ",sizeof("Content-Length: ")-1))
			flen=atoi(sb.sb_start+sizeof("Content-Length: ")-1);
		else if(!strncasecmp(sb.sb_start,"Last-Modified: ",sizeof("Last-Modified: ")-1))
		{
			*p2='\0';
			strcpy(http->date,sb.sb_start+sizeof("Last-Modified: ")-1);
		}
		p2+=2;
		sb.sb_size-=p2-sb.sb_start;
		sb.sb_start=p2;
		if(sb.sb_size<1)
		{
			if(RTMPSockBuf_Fill(&sb)<1)
			{
				ret=HTTPRES_LOST_CONNECTION;
				goto leave;
			}
		}
	}

	len_known=flen>0;
	while((!len_known||flen>0)&&(sb.sb_size>0||RTMPSockBuf_Fill(&sb)>0))
	{
		cb(sb.sb_start,1,sb.sb_size,http->data);
		if(len_known)
			flen-=sb.sb_size;
		http->size+=sb.sb_size;
		sb.sb_size=0;
	}

	if(flen>0)
		ret=HTTPRES_LOST_CONNECTION;

	leave:
	RTMPSockBuf_Close(&sb);
	return ret;
}

#ifdef CRYPTO

#define CHUNK	16384

struct info
{
	z_stream *zs;
	HMAC_CTX ctx;
	int first;
	int zlib;
	int size;
};

static size_t
swfcrunch(void *ptr,size_t size,size_t nmemb,void *stream)
{
	struct info *i=stream;
	char *p=ptr;
	size_t len=size *nmemb;

	if(i->first)
	{
		i->first=0;
		/* compressed? */
		if(!strncmp(p,"CWS",3))
		{
			*p='F';
			i->zlib=1;
		}
		HMAC_crunch(i->ctx,(unsigned char*)p,8);
		p+=8;
		len-=8;
		i->size=8;
	}

	if(i->zlib)
	{
		unsigned char out[CHUNK];
		i->zs->next_in=(unsigned char*)p;
		i->zs->avail_in=len;
		do
		{
			i->zs->avail_out=CHUNK;
			i->zs->next_out=out;
			inflate(i->zs,Z_NO_FLUSH);
			len=CHUNK-i->zs->avail_out;
			i->size+=len;
			HMAC_crunch(i->ctx,out,len);
		}
		while(i->zs->avail_out==0);
	}
	else
	{
		i->size+=len;
		HMAC_crunch(i->ctx,(unsigned char*)p,len);
	}
	return size*nmemb;
}

static int tzoff;
static int tzchecked;

#define	JAN02_1980	318340800

static const char *monthtab[12]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
static const char *days[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};

/* Parse an HTTP datestamp into Unix time */
static time_t
make_unix_time(char *s)
{
	struct tm time;
	int i,ysub=1900,fmt=0;
	char *month;
	char *n;
	time_t res;

	if(s[3]!=' ')
	{
		fmt=1;
		if(s[3]!=',')
			ysub=0;
	}
	for(n=s;*n;++n)
		if(*n=='-'||*n==':')
			*n=' ';

	time.tm_mon=0;
	n=strchr(s,' ');
	if(fmt)
	{
		/* Day, DD-MMM-YYYY HH:MM:SS GMT */
		time.tm_mday=strtol(n+1,&n,0);
		month=n+1;
		n=strchr(month,' ');
		time.tm_year=strtol(n+1,&n,0);
		time.tm_hour=strtol(n+1,&n,0);
		time.tm_min=strtol(n+1,&n,0);
		time.tm_sec=strtol(n+1,NULL,0);
	}
	else
	{
		/* Unix ctime() format. Does not conform to HTTP spec. */
		/* Day MMM DD HH:MM:SS YYYY */
		month=n+1;
		n=strchr(month,' ');
		while(isspace(*n))
			n++;
		time.tm_mday=strtol(n,&n,0);
		time.tm_hour=strtol(n+1,&n,0);
		time.tm_min=strtol(n+1,&n,0);
		time.tm_sec=strtol(n+1,&n,0);
		time.tm_year=strtol(n+1,NULL,0);
	}
	if(time.tm_year>100)
		time.tm_year-=ysub;

	for(i=0;i<12;i++)
		if(!strncasecmp(month,monthtab[i],3))
		{
			time.tm_mon=i;
			break;
		}
	time.tm_isdst=0;		/* daylight saving is never in effect in GMT */

	/* this is normally the value of extern int timezone, but some
	 * braindead C libraries don't provide it.
	 */
	if(!tzchecked)
	{
		struct tm *tc;
		time_t then=JAN02_1980;
		tc=localtime(&then);
		tzoff=(12-tc->tm_hour)*3600+tc->tm_min*60+tc->tm_sec;
		tzchecked=1;
	}
	res=mktime(&time);
	/* Unfortunately, mktime() assumes the input is in local time,
	 * not GMT, so we have to correct it here.
	 */
	if(res!=-1)
		res+=tzoff;
	return res;
}

/* Convert a Unix time to a network time string
 * Weekday, DD-MMM-YYYY HH:MM:SS GMT
 */
static void
strtime(time_t *t,char *s)
{
	struct tm *tm;

	tm=gmtime((time_t*)t);
	sprintf(s,"%s, %02d %s %d %02d:%02d:%02d GMT",days[tm->tm_wday],tm->tm_mday,monthtab[tm->tm_mon],tm->tm_year+1900,tm->tm_hour,tm->tm_min,tm->tm_sec);
}

#define HEX2BIN(a)  	(((a)&0x40)?((a)&0xf)+9:((a)&0xf))

int
RTMP_HashSWF(const char *url,unsigned int *size,unsigned char *hash,int age)
{
	FILE *f=NULL;
	char *path,date[64],cctim[64];
	long pos=0;
	time_t ctim= -1,cnow;
	int i,got=0,ret=0;
	unsigned int hlen;
	struct info in={0};
	struct HTTP_ctx http={0};
	HTTPResult httpres;
	z_stream zs={0};
	AVal home,hpre;

	date[0]='\0';
#ifdef _WIN32
#ifdef XBMC4XBOX
	hpre.av_val="Q:";
	hpre.av_len=2;
	home.av_val="\\UserData";
#else
	hpre.av_val=getenv("HOMEDRIVE");
	hpre.av_len=strlen(hpre.av_val);
	home.av_val=getenv("HOMEPATH");
#endif
#define DIRSEP	"\\"

#else /* !_WIN32 */
	hpre.av_val="";
	hpre.av_len=0;
	home.av_val=getenv("HOME");
#define DIRSEP	"/"
#endif
	if(!home.av_val)
		home.av_val=".";
	home.av_len=strlen(home.av_val);

	/* SWF hash info is cached in a fixed-format file.
	 * url: <url of SWF file>
	 * ctim: HTTP datestamp of when we last checked it.
	 * date: HTTP datestamp of the SWF's last modification.
	 * size: SWF size in hex
	 * hash: SWF hash in hex
	 *
	 * These fields must be present in this order. All fields
	 * besides URL are fixed size.
	 */
	path=malloc(hpre.av_len+home.av_len+sizeof(DIRSEP ".swfinfo"));
	sprintf(path,"%s%s" DIRSEP ".swfinfo",hpre.av_val,home.av_val);

	f=fopen(path,"r+");
	while(f)
	{
		char buf[4096],*file,*p;

		file=strchr(url,'/');
		if(!file)
			break;
		file+=2;
		file=strchr(file,'/');
		if(!file)
			break;
		file++;
		hlen=file-url;
		p=strrchr(file,'/');
		if(p)
			file=p;
		else
			file--;

		while(fgets(buf,sizeof(buf),f))
		{
			char *r1;

			got=0;

			if(strncmp(buf,"url: ",5))
				continue;
			if(strncmp(buf+5,url,hlen))
				continue;
			r1=strrchr(buf,'/');
			i=strlen(r1);
			r1[--i]='\0';
			if(strncmp(r1,file,i))
				continue;
			pos=ftell(f);
			while(got<4&&fgets(buf,sizeof(buf),f))
			{
				if(!strncmp(buf,"size: ",6))
				{
					*size=strtol(buf+6,NULL,16);
					got++;
				}
				else if(!strncmp(buf,"hash: ",6))
				{
					unsigned char*ptr=hash,*in=(unsigned char*)buf+6;
					int l=strlen((char *)in)-1;
					for(i=0;i<l;i+=2)
						*ptr++=(HEX2BIN(in[i])<<4)|HEX2BIN(in[i+1]);
					got++;
				}
				else if(!strncmp(buf,"date: ",6))
				{
					buf[strlen(buf)-1]='\0';
					strncpy(date,buf+6,sizeof(date));
					got++;
				}
				else if(!strncmp(buf,"ctim: ",6))
				{
					buf[strlen(buf)-1]='\0';
					ctim=make_unix_time(buf+6);
					got++;
				}
				else if(!strncmp(buf,"url: ",5))
					break;
			}
			break;
		}
		break;
	}

	cnow=time(NULL);
	/* If we got a cache time, see if it's young enough to use directly */
	if(age&&ctim>0)
	{
		ctim=cnow-ctim;
		ctim/=3600*24;	/* seconds to days */
		if(ctim<age)		/* ok, it's new enough */
			goto out;
	}

	in.first=1;
	HMAC_setup(in.ctx,"Genuine Adobe Flash Player 001",30);
	inflateInit(&zs);
	in.zs= &zs;

	http.date=date;
	http.data= &in;

	httpres=HTTP_get(&http,url,swfcrunch);

	inflateEnd(&zs);

	if(httpres!=HTTPRES_OK&&httpres!=HTTPRES_OK_NOT_MODIFIED)
	{
		ret= -1;
		if(httpres==HTTPRES_LOST_CONNECTION)
			RTMP_Log(RTMP_LOGERROR,"%s: connection lost while downloading swfurl %s",__FUNCTION__,url);
		else if(httpres==HTTPRES_NOT_FOUND)
			RTMP_Log(RTMP_LOGERROR,"%s: swfurl %s not found",__FUNCTION__,url);
		else
			RTMP_Log(RTMP_LOGERROR,"%s: couldn't contact swfurl %s (HTTP error %d)",__FUNCTION__,url,http.status);
	}
	else
	{
		if(got&&pos)
			fseek(f,pos,SEEK_SET);
		else
		{
			char *q;
			if(!f)
				f=fopen(path,"w");
			if(!f)
			{
				int err=errno;
				RTMP_Log(RTMP_LOGERROR,"%s: couldn't open %s for writing, errno %d (%s)",__FUNCTION__,path,err,strerror(err));
				ret= -1;
				goto out;
			}
			fseek(f,0,SEEK_END);
			q=strchr(url,'?');
			if(q)
				i=q-url;
			else
				i=strlen(url);

			fprintf(f,"url: %.*s\n",i,url);
		}
		strtime(&cnow,cctim);
		fprintf(f,"ctim: %s\n",cctim);

		if(!in.first)
		{
			HMAC_finish(in.ctx,hash,hlen);
			*size=in.size;

			fprintf(f,"date: %s\n",date);
			fprintf(f,"size: %08x\n",in.size);
			fprintf(f,"hash: ");
			for(i=0;i<SHA256_DIGEST_LENGTH;i++)
				fprintf(f,"%02x",hash[i]);
			fprintf(f,"\n");
		}
	}
	HMAC_close(in.ctx);
	out:
	free(path);
	if(f)
		fclose(f);
	return ret;
}
#else
int
RTMP_HashSWF(const char *url,unsigned int *size,unsigned char *hash,int age)
{
	return -1;
}
#endif
